#ifndef MATRIX_INC
#define MATRIX_INC

#include <mpi.h>
#include "Defines.h"

template<class T>
class Matrix;

template<class T>
istream& operator >>(istream& is, const Matrix<T>& m);

template<class T>
ostream& operator <<(ostream& os, const Matrix<T>& m);

/*
 * =====================================================================================
 *        Class:  Matrix
 *  Description:  Classe que representa uma matriz
 * =====================================================================================
 */
template<class T>
class Matrix
{

public:

    /*-----------------------------------------------------------------------------
     *  Contrói uma nova matriz rows x cols
     *-----------------------------------------------------------------------------*/
    Matrix(uint rows, uint cols = 1);

    /*-----------------------------------------------------------------------------
     *  Destróia a matriz
     *-----------------------------------------------------------------------------*/
    virtual ~Matrix();

    /*-----------------------------------------------------------------------------
     *  Retorna uma referência para o valor (i,j) da matriz
     *-----------------------------------------------------------------------------*/
    T& operator ()(uint i, uint j = 0) { return data[i * cols + j]; }

    /*-----------------------------------------------------------------------------
     *  Preenche a matriz com 0s
     *-----------------------------------------------------------------------------*/
    void fillZeros(uint init = 0, uint size = 0);

    /*-----------------------------------------------------------------------------
     *  Retorna o número de linhas
     *-----------------------------------------------------------------------------*/
    uint nRows() { return rows; }

    /*-----------------------------------------------------------------------------
     *  Retorna o número de colunas
     *-----------------------------------------------------------------------------*/
    uint nCols() { return cols; }

    /*-----------------------------------------------------------------------------
     *  Retorna o tamanho = linhas x colunas
     *-----------------------------------------------------------------------------*/
    ulong size() { return rows * cols; }

    /*-----------------------------------------------------------------------------
     *  Envia uma parte da matriz para o rank anterior
     *-----------------------------------------------------------------------------*/
    void send(uint dest, uint init, uint size);

    /*-----------------------------------------------------------------------------
     *  Recebe uma parte da matriz do próximo rank
     *-----------------------------------------------------------------------------*/
    void recv(uint source, uint init, uint size);

    /*-----------------------------------------------------------------------------
     *  Adiciona os valores da matriz a partir de um stream de entrada
     *-----------------------------------------------------------------------------*/
    friend istream& operator >> <>(istream& is, const Matrix<T>& m);

    /*-----------------------------------------------------------------------------
     *  Imprime os valores em um stream de saída
     *-----------------------------------------------------------------------------*/
    friend ostream& operator << <>(ostream& os, const Matrix<T>& m);

private:

    /*-----------------------------------------------------------------------------
     *  Vetor de valores
     *-----------------------------------------------------------------------------*/
    T* data;

    /*-----------------------------------------------------------------------------
     *  Linhas
     *-----------------------------------------------------------------------------*/
    uint rows;

    /*-----------------------------------------------------------------------------
     *  Colunas
     *-----------------------------------------------------------------------------*/
    uint cols;

};

/*-----------------------------------------------------------------------------
 *  Matriz de double
 *-----------------------------------------------------------------------------*/
typedef Matrix<double> mdouble;

/*-----------------------------------------------------------------------------
 *  Ponteiro para matriz de double
 *-----------------------------------------------------------------------------*/
typedef mdouble* mdoublePtr;

/*
 *--------------------------------------------------------------------------------------
 *       Class:  Matrix
 *      Method:  Matrix
 * Description:  Contrói uma nova matriz rows x cols
 *--------------------------------------------------------------------------------------
 */
template<class T>
inline Matrix<T>::Matrix(uint rows, uint cols)
{
    this->rows = rows;
    this->cols = cols;
    this->data = new T[rows * cols]();
}

/*
 *--------------------------------------------------------------------------------------
 *       Class:  Matrix
 *      Method:  ~Matrix
 * Description:  Destrói a matriz
 *--------------------------------------------------------------------------------------
 */
template<class T>
inline Matrix<T>::~Matrix()
{
    delete[] data;
}

/*
 *--------------------------------------------------------------------------------------
 *       Class:  Matrix
 *      Method:  fillZeros
 * Description:  Preenche a matriz com 0s
 *--------------------------------------------------------------------------------------
 */
template<class T>
inline void Matrix<T>::fillZeros(uint init, uint size)
{
    if(size == 0)
        size = rows * cols - init;
    for(uint i = init; i < init + size; i++)
        data[i] = 0;
}

/*
 *--------------------------------------------------------------------------------------
 *       Class:  Matrix
 *      Method:  send
 * Description:  Envia uma parte da matriz para o rank anterior
 *--------------------------------------------------------------------------------------
 */
template<>
inline void Matrix<double>::send(uint dest, uint init, uint size)
{
    double *buf = &(data[init]);
    MPI::COMM_WORLD.Isend(buf, size, MPI::DOUBLE, dest, 1);
}

/*
 *--------------------------------------------------------------------------------------
 *       Class:  Matrix
 *      Method:  recv
 * Description:  Recebe uma parte da matriz do próximo rank
 *--------------------------------------------------------------------------------------
 */
template<>
inline void Matrix<double>::recv(uint source, uint init, uint size)
{
    double *buf = &(data[init]);
    return MPI::COMM_WORLD.Recv(buf, size, MPI::DOUBLE, source, 1);
}

/*
 *--------------------------------------------------------------------------------------
 *       Class:  Matrix
 *      Method:  operator >>
 * Description:  Adiciona os valores da matriz a partir de um stream de entrada
 *--------------------------------------------------------------------------------------
 */
template<class T>
istream& operator >>(istream& is, const Matrix<T>& m)
{
    ulong size = m.rows * m.cols;
    for(ulong i = 0; i < size; i++)
        is >> m.data[i];
    return is;
}

/*
 *--------------------------------------------------------------------------------------
 *       Class:  Matrix
 *      Method:  operator <<
 * Description:  Imprime os valores em um stream de saída
 *--------------------------------------------------------------------------------------
 */
template<class T>
ostream& operator <<(ostream& os, const Matrix<T>& m)
{
    for(uint i = 0; i < m.rows; i++)
    {
        for(uint j = 0; j < m.cols; j++)
            os << m.data[i * m.cols + j] << " ";
        os << endl;
    }
    return os;
}

#endif
